/**
 * 
 */
package edu.cmu.mism.dgjava.algorithm.impl;

import org.eclipse.core.runtime.IProgressMonitor;

import edu.cmu.mism.dgjava.data.models.option.BaseType;
import edu.cmu.mism.dgjava.data.models.option.Equity;
import edu.cmu.mism.dgjava.data.models.option.ImplicitFiniteDifference;
import edu.cmu.mism.dgjava.data.models.option.OptionCalculationResult;
import edu.cmu.mism.dgjava.data.models.option.impl.OptionModelFactoryImpl;

/**
 * @author Ankit
 *
 */
public class ImplicitFiniteDifferenceAlg extends OptionsAlg{

	private int priceIntervals;
	private int timeIntervals;
	private double maxPrice;
	
	private double optionValue = 0.0;
	private double actualOptionValue = 0.0;
	
	private double deltaTimeIntervals = 0.0;
	private double deltaMaxPrice = 0.0;
	
	private double finiteDifferenceGrid[][];
	private double stockPriceArray[];
	private double a[];
	private double b[];
	private double c[];
	private double r[];
	private double u[];
	
	private int stockIndex = 0;
	
	public ImplicitFiniteDifferenceAlg(Object underlyingModel,
			Object pricingModel) {
		super(underlyingModel, pricingModel);
	}
	
	/* (non-Javadoc)
	 * @see edu.cmu.mism.dgjava.algorithm.IOptionAlgorithm#calculate(org.eclipse.core.runtime.IProgressMonitor)
	 */
	@Override
	public OptionCalculationResult calculate(IProgressMonitor monitor) {
		
		OptionCalculationResult result = OptionModelFactoryImpl.eINSTANCE
		.createOptionCalculationResult();
		
		initializeOptionsVariable();
		calculateOption();
		actualOptionValue = optionValue;
		result.setPrice(optionValue);
		calculateGreekLetters();
		result.setDelta(delta);
		result.setGamma(gamma);
		result.setTheta(theta);
		result.setVega(vega);
		result.setRho(rho);
		return result;
	}

	/**
	 * Initialize all the options variables entered by the user and build the grids and arrays.
	 */
	private void initializeOptionsVariable() {

		stockPrice = ((Equity)_underlyingModel).getStockPrice();
		riskFreeRate = ((Equity)_underlyingModel).getRiskFreeRate()/100.0;
		volatility = ((Equity)_underlyingModel).getVolatility()/100.0;

		strikePrice = ((ImplicitFiniteDifference)_pricingModel).getExercisePrice();
		timeToExpiration = ((ImplicitFiniteDifference)_pricingModel).getTimeToExpire();

		type = ((ImplicitFiniteDifference)_pricingModel).getType();
		
		maxPrice = ((ImplicitFiniteDifference)_pricingModel).getMaxPrice();
		timeIntervals = ((ImplicitFiniteDifference)_pricingModel).getTimeIntervals();
		priceIntervals = ((ImplicitFiniteDifference)_pricingModel).getPriceIntervals();

		
		deltaTimeIntervals = timeToExpiration/timeIntervals;
		deltaMaxPrice = maxPrice/priceIntervals;
		
		finiteDifferenceGrid = new double[timeIntervals + 1][];
		stockPriceArray = new double[priceIntervals + 1];
		a = new double[priceIntervals - 1];
		b = new double[priceIntervals - 1];
		c = new double[priceIntervals - 1];
		r = new double[priceIntervals - 1];
		u = new double[priceIntervals - 1];
		
		// prepare the finiteDifferenceGrid
		for(int i = 0; i <= timeIntervals; i++ ){
			finiteDifferenceGrid[i] = new double[priceIntervals + 1];
			for(int j = 0; j <= priceIntervals; j++){
				finiteDifferenceGrid[i][j] = 0.0;
			}
		}
		
	}
	
	/* (non-Javadoc)
	 * @see edu.cmu.mism.dgjava.algorithm.impl.OptionsAlg#calculateOption()
	 */
	@Override
	protected void calculateOption() {

		// Initialize stock price array
		for(int j = 0; j <= priceIntervals; j++){
			stockPriceArray[j] = j*maxPrice/priceIntervals;
		}
		
		// Initialize stock price at zero and maxPrice
		for(int i = 0; i <= timeIntervals; i++ ){
			finiteDifferenceGrid[i][0] = strikePrice;
			finiteDifferenceGrid[i][priceIntervals] = 0.0;
		}
		
		// Initialize option value at maturity
		for(int j = 0; j <= priceIntervals; j++){
			
			finiteDifferenceGrid[timeIntervals][j] = calculateOptionPrice(stockPriceArray[j],strikePrice, 0.0);
		}
		
		// Calculate the interior values to be sent to tridiagonal algorithm
		for(int j = 0; j < priceIntervals - 1; j++){
			
			a[j] = aj(j + 1, deltaTimeIntervals);
			b[j] = bj(j + 1, deltaTimeIntervals);
			c[j] = cj(j + 1, deltaTimeIntervals);
			
		}
		
		// Loop over columns from timeIntervals - 1 to zero
		
		for (int i = 1; i <= timeIntervals; i++){
			for(int j = 0; j < priceIntervals - 1; j++){
				r[j] = finiteDifferenceGrid[timeIntervals - i + 1][j + 1];
			}
			r[0] = r[0] - a[0]*finiteDifferenceGrid[timeIntervals - i][0];
			r[priceIntervals - 2] = r[priceIntervals - 2] - c[priceIntervals - 2]*finiteDifferenceGrid[timeIntervals - i][priceIntervals];
			
			// Solve the tridiagonal system of equations. Solutions kept in u
			
			tridiag();
			
			for(int j = 1; j < priceIntervals; j++){
				
				finiteDifferenceGrid[timeIntervals - i][j] = updateOptionGridValues(j);
				
			}
		}
		
		int j = 0;
		while(stockPriceArray[j] < stockPrice){
			j++;
		}
		
		if(stockPriceArray[j] == stockPrice){
			optionValue = finiteDifferenceGrid[0][j];
		} else {
			optionValue = finiteDifferenceGrid[0][j - 1] + (finiteDifferenceGrid[0][j] - finiteDifferenceGrid[0][j - 1]) * (stockPrice - stockPriceArray[j - 1])/(stockPriceArray[j] - stockPriceArray[j - 1]);
		}
		if(type.getValue() == BaseType.CALL_VALUE){
			optionValue *= -1;
		}
		
		stockIndex = j;
		
		theta = ((finiteDifferenceGrid[2][j] - finiteDifferenceGrid[0][j])/(2*deltaTimeIntervals))/365.0;
		
	}
	

	/**
	 * @param index
	 * @return
	 */
	private double updateOptionGridValues(int index) {
		
		if(type.getValue() == BaseType.PUT_VALUE){
			if(u[index - 1] < strikePrice - index*deltaMaxPrice){
				return strikePrice - index*deltaMaxPrice; 
			} else {
				return u[index - 1];
			}
		} else{
			if(u[index - 1] > strikePrice - index*deltaMaxPrice){
				return strikePrice - index*deltaMaxPrice; 
			} else {
				return u[index - 1];
			}
		}
		
	}


	/**
	 * @param stockPrice
	 * @param strikePrice
	 * @param comparer
	 * @return
	 */
	private double calculateOptionPrice(double stockPrice, double strikePrice, double comparer) {
		
		if(type.getValue() == BaseType.PUT_VALUE){
			return max(strikePrice - stockPrice, comparer);
		} else {
			return max(stockPrice - strikePrice, comparer);
		}
	}

	/**
	 * 
	 */
	private void tridiag() {
		
		double bet;
		double gam[] = new double[priceIntervals - 1];
		
		u[0] = r[0]/(bet = b[0]);
		
		for(int j = 1; j < priceIntervals - 1; j++){
			gam[j] = c[j - 1]/bet;
			bet = b[j] - a[j] * gam[j];
			u[j] = (r[j] - a[j] * u[j - 1])/bet;
		}
		for(int j = (priceIntervals - 3); j >= 0; j-- ){
			u[j] -= gam[j + 1] * u[j + 1]; 
		}
		
	}

	/**
	 * @param i
	 * @param deltaTimeIntervals
	 * @return
	 */
	private double aj(int i, double deltaTimeIntervals) {
		return (0.5*riskFreeRate*i*deltaTimeIntervals -
				0.5*volatility*volatility*i*i*deltaTimeIntervals);
	}
	
	/**
	 * @param i
	 * @param deltaTimeIntervals
	 * @return
	 */
	private double bj(int i, double deltaTimeIntervals) {
		return (1.0 + volatility*volatility*i*i*deltaTimeIntervals + riskFreeRate*deltaTimeIntervals);
	}
	
	/**
	 * @param i
	 * @param deltaTimeIntervals
	 * @return
	 */
	private double cj(int i, double deltaTimeIntervals) {
		return (-0.5*riskFreeRate*i*deltaTimeIntervals - 0.5*volatility*volatility*i*i*deltaTimeIntervals);
	}


	/* (non-Javadoc)
	 * @see edu.cmu.mism.dgjava.algorithm.impl.OptionsAlg#calculateDelta()
	 */
	@Override
	protected void calculateDelta() {
		
		delta = (finiteDifferenceGrid[0][stockIndex + 1] - finiteDifferenceGrid[0][stockIndex - 1])/(2*deltaMaxPrice);
		
	}

	/* (non-Javadoc)
	 * @see edu.cmu.mism.dgjava.algorithm.impl.OptionsAlg#calculateGamma()
	 */
	@Override
	protected void calculateGamma() {
		
		gamma = (finiteDifferenceGrid[0][stockIndex + 1] + finiteDifferenceGrid[0][stockIndex - 1] - 2 * finiteDifferenceGrid[0][stockIndex])/(deltaMaxPrice * deltaMaxPrice);
	}

	/* (non-Javadoc)
	 * @see edu.cmu.mism.dgjava.algorithm.impl.OptionsAlg#calculateRho()
	 */
	@Override
	protected void calculateRho() {
		
		initializeOptionsVariable();
		riskFreeRate = riskFreeRate + 0.01;
		calculateOption();
		rho = (optionValue - actualOptionValue)/(0.01*100);
		
	}

	/* (non-Javadoc)
	 * @see edu.cmu.mism.dgjava.algorithm.impl.OptionsAlg#calculateTheta()
	 */
	@Override
	protected void calculateTheta() {
		
		theta = ((finiteDifferenceGrid[1][stockIndex] - finiteDifferenceGrid[0][stockIndex])/deltaTimeIntervals)/365.0;
	}

	/* (non-Javadoc)
	 * @see edu.cmu.mism.dgjava.algorithm.impl.OptionsAlg#calculateVega()
	 */
	@Override
	protected void calculateVega() {
		
		initializeOptionsVariable();
		volatility = volatility + 0.01;
		calculateOption();
		vega = (optionValue - actualOptionValue)/(0.01*100);
	}

}
